"use strict";

/**
 * 正则表达式
 * 原文链接：
 * https://juejin.im/post/5965943ff265da6c30653879#heading-21
 */
//  第一章 正则表达式字符匹配攻略
// 正则表达式是匹配模式，要么匹配字符，要么匹配位置。请记住这句话。

// 然而关于正则如何匹配字符的学习，大部分人都觉得这块比较杂乱。

// 毕竟元字符太多了，看起来没有系统性，不好记。本章就解决这个问题。

// 内容包括：

// 两种模糊匹配
// 字符组
// 量词
// 分支结构
// 案例分析
// 1. 两种模糊匹配
// 如果正则只有精确匹配是没多大意义的，比如/hello/，也只能匹配字符串中的”hello”这个子串。

var regex = /hello/;
console.log(regex.test("hello"));   // true

//  正则表达式之所以强大，是因为其能实现模糊匹配。

// 而模糊匹配，有两个方向上的“模糊”：横向模糊和纵向模糊。

// 1.1 横向模糊匹配

// 横向模糊指的是，一个正则可匹配的字符串的长度不是固定的，可以是多种情况的。

// 其实现的方式是使用量词。譬如{m,n}，表示连续出现最少m次，最多n次。

// 比如/ab{2,5}c/表示匹配这样一个字符串：第一个字符是“a”，接下来是2到5个字符“b”，最后是字符“c”。测试如下：

var regex1 = /ab{2,5}c/g;
var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
console.log(string.match(regex1));

// 注意：案例中用的正则是/ab{2,5}c/g，后面多了g，它是正则的一个修饰符。表示全局匹配，即在目标字符串中按顺序找到满足匹配模式的所有子串，强调的是“所有”，而不只是“第一个”。g是单词global的首字母。

// 1.2 纵向模糊匹配

// 纵向模糊指的是，一个正则匹配的字符串，具体到某一位字符时，它可以不是某个确定的字符，可以有多种可能。

// 其实现的方式是使用字符组。譬如[abc]，表示该字符是可以字符“a”、“b”、“c”中的任何一个。

// 比如/a[123]b/可以匹配如下三种字符串：”a1b”、”a2b”、”a3b”。测试如下：


var regex2 = /a[123]b/g;
var string2 = "a0b a1b a2b a3b a4b";
console.log(string2.match(regex2));

//  以上就是本章讲的主体内容，只要掌握横向和纵向模糊匹配，就能解决很大部分正则匹配问题。

// 接下来的内容就是展开说了，如果对此都比较熟悉的话，可以跳过，直接看本章案例那节。

// 2. 字符组
// 需要强调的是，虽叫字符组（字符类），但只是其中一个字符。例如[abc]，表示匹配一个字符，它可以是“a”、“b”、“c”之一。

// 2.1 范围表示法

// 如果字符组里的字符特别多的话，怎么办？可以使用范围表示法。

// 比如[123456abcdefGHIJKLM]，可以写成[1-6a-fG-M]。用连字符-来省略和简写。

// 因为连字符有特殊用途，那么要匹配“a”、“-”、“z”这三者中任意一个字符，该怎么做呢？

// 不能写成[a-z]，因为其表示小写字符中的任何一个字符。

// 可以写成如下的方式：[-az]或[az-]或[a\-z]。即要么放在开头，要么放在结尾，要么转义。总之不会让引擎认为是范围表示法就行了。

// 2.2 排除字符组

// 纵向模糊匹配，还有一种情形就是，某位字符可以是任何东西，但就不能是”a”、”b”、”c”。

// 此时就是排除字符组（反义字符组）的概念。例如[^abc]，表示是一个除”a”、”b”、”c”之外的任意一个字符。字符组的第一位放^（脱字符），表示求反的概念。

// 当然，也有相应的范围表示法。

// 2.3 常见的简写形式

// 有了字符组的概念后，一些常见的符号我们也就理解了。因为它们都是系统自带的简写形式。

// \d就是[0-9]。表示是一位数字。记忆方式：其英文是digit（数字）。

// \D就是[^0-9]。表示除数字外的任意字符。

// \w就是[0-9a-zA-Z_]。表示数字、大小写字母和下划线。记忆方式：w是word的简写，也称单词字符。

// \W是[^0-9a-zA-Z_]。非单词字符。

// \s是[ \t\v\n\r\f]。表示空白符，包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。记忆方式：s是space character的首字母。

// \S是[^ \t\v\n\r\f]。 非空白符。

// .就是[^\n\r\u2028\u2029]。通配符，表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。记忆方式：想想省略号…中的每个点，都可以理解成占位符，表示任何类似的东西。

// 如果要匹配任意字符怎么办？可以使用[\d\D]、[\w\W]、[\s\S]和[^]中任何的一个。

// 3. 量词
// 量词也称重复。掌握{m,n}的准确含义后，只需要记住一些简写形式。

// 3.1 简写形式

// **{m,}** 表示至少出现m次。

// **{m}** 等价于{m,m}，表示出现m次。

// **?** 等价于{0,1}，表示出现或者不出现。记忆方式：问号的意思表示，有吗？

// **+**等价于{1,}，表示出现至少一次。记忆方式：加号是追加的意思，得先有一个，然后才考虑追加。

// ***** 等价于{0,}，表示出现任意次，有可能不出现。记忆方式：看看天上的星星，可能一颗没有，可能零散有几颗，可能数也数不过来。

// 3.2 贪婪匹配和惰性匹配

// 看如下的例子：

var regex3 = /\d{2,5}/g;
var string3 = "123 1234 12345 123456";
console.log(string3.match(regex3));

// 其中正则/\d{2,5}/，表示数字连续出现2到5次。会匹配2位、3位、4位、5位连续数字。

// 但是其是贪婪的，它会尽可能多的匹配。你能给我6个，我就要5个。你能给我3个，我就3要个。反正只要在能力范围内，越多越好。

// 我们知道有时贪婪不是一件好事（请看文章最后一个例子）。而惰性匹配，就是尽可能少的匹配：

var regex4 = /\d{2,5}?/g;
var string4 = "123 1234 12345 123456";
console.log(string4.match(regex4));
// [ '12', '12', '34', '12', '34', '12', '34', '56' ]

// 其中/\d{2,5}?/表示，虽然2到5次都行，当2个就够的时候，就不在往下尝试了。

// 通过在量词后面加个问号就能实现惰性匹配，因此所有惰性匹配情形如下：

// **{m,n}?**
// **{m,}?**
// **??**
// **+?**
// ***?**

// 对惰性匹配的记忆方式是：量词后面加个问号，问一问你知足了吗，你很贪婪吗？

// 4. 多选分支
// 一个模式可以实现横向和纵向模糊匹配。而多选分支可以支持多个子模式任选其一。

// 具体形式如下：(p1|p2|p3)，其中p1、p2和p3是子模式，用|（管道符）分隔，表示其中任何之一。

// 例如要匹配”good”和”nice”可以使用/good|nice/。测试如下：


var regex5 = /good|nice/g;
var string5 = "good idea,nice try.";
console.log(string5.match(regex5));
// [ 'good', 'nice' ]

// 但有个事实我们应该注意，比如我用/good|goodbye/，去匹配”goodbye”字符串时，结果是”good”：

var regex6 = /good|goodbye/g;
var string6 = "goodbye";
console.log(string6.match(regex6));
// [ 'good' ]

// 而把正则改成/goodbye|good/，结果是：
var regex7 = /goodbye|good/g;
var string7 = "goodbye";
console.log(string7.match(regex7));
// [ 'goodbye' ]

// 也就是说，分支结构也是惰性的，即当前面的匹配上了，后面的就不再尝试了。

// 5. 案例分析
// 匹配字符，无非就是字符组、量词和分支结构的组合使用罢了。

// 下面找几个例子演练一下（其中，每个正则并不是只有唯一写法）：

// 5.1 匹配16进制颜色值

// 要求匹配：

// #ffbbad

// #Fc01DF

// #FFF

// #ffE

// 分析：

// 表示一个16进制字符，可以用字符组[0-9a-fA-F]。

// 其中字符可以出现3或6次，需要是用量词和分支结构。

// 使用分支结构时，需要注意顺序。

// 正则如下：

var regex8 = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
var string8 = "#ffbbad #Fc01DF #FFF #ffE";
console.log(string8.match(regex8));
// [ '#ffbbad', '#Fc01DF', '#FFF', '#ffE' ]

//  5.2 匹配时间

// 以24小时制为例。

// 要求匹配：

// 23:59

// 02:07

// 分析：

// 共4位数字，第一位数字可以为[0-2]。

// 当第1位为2时，第2位可以为[0-3]，其他情况时，第2位为[0-9]。

// 第3位数字为[0-5]，第4位为[0-9]

// 正则如下：

var regex9 = /^([01][0-9]|[2][0-3]):[0-5][0-9]$/;
console.log(regex9.test("23:59"));  // true
console.log(regex9.test("02:07"));  // true

// 如果也要求匹配7:9，也就是说时分前面的0可以省略。
// 此时正则变成：

var regex10 = /^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/;
console.log(regex10.test("23:59")); // true
console.log(regex10.test("02:07")); // true
console.log(regex10.test("7:9"));  // true

// 5.3 匹配日期

// 比如yyyy-mm-dd格式为例。

// 要求匹配：

// 2017-06-10

// 分析：

// 年，四位数字即可，可用[0-9]{4}。

// 月，共12个月，分两种情况01、02、……、09和10、11、12，可用(0[1-9]|1[0-2])。

// 日，最大31天，可用(0[1-9]|[12][0-9]|3[01])。

// 正则如下：

var regex11 = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
console.log(regex11.test("2017-06-10")); // true
console.log(regex11.test("2017-16-10")); // false

//  5.4 window操作系统文件路径

// 要求匹配：

// F:\study\javascript\regex\regular expression.pdf

// F:\study\javascript\regex\

// F:\study\javascript

// F:\

// 分析：

// 整体模式是: 盘符:\文件夹\文件夹\文件夹\

// 其中匹配F:\，需要使用[a-zA-Z]:\\，其中盘符不区分大小写，注意\字符需要转义。

// 文件名或者文件夹名，不能包含一些特殊字符，此时我们需要排除字符组[^\\:*<>|"?\r\n/]来表示合法字符。另外不能为空名，至少有一个字符，也就是要使用量词+。因此匹配“文件夹\”，可用[^\\:*<>|"?\r\n/]+\\。

// 另外“文件夹\”，可以出现任意次。也就是([^\\:*<>|"?\r\n/]+\\)*。其中括号提供子表达式。

// 路径的最后一部分可以是“文件夹”，没有\，因此需要添加([^\\:*<>|"?\r\n/]+)?。

// 最后拼接成了一个看起来比较复杂的正则：

var regex12 = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/
console.log(regex12.test("F:\\study\\javascript\\regex\\regular expression.pdf"));
console.log(regex12.test("F:\\study\\javascript\\regex\\"));
console.log(regex12.test("F:\\study\\javascript"));
console.log(regex12.test("F:\\"));
console.log(regex12.test("/F:\\09dsa\\ %#$#@ dsa\\ \\")); // false

// 其中，JS中字符串表示\时，也要转义。

// 5.5 匹配id
// 要求从 <div id="container" class="main"></div>
// 提取出id=”container”。
// 可能最开始想到的正则是：

var regex13 = /id=".*"/;
var string13 = '<div id="container" class="main"></div>';
console.log(string13.match(regex13)[0]);
// id="container" class="main"

// 因为.是通配符，本身就匹配双引号的，而量词*又是贪婪的，当遇到container后面双引号时，不会停下来，会继续匹配，直到遇到最后一个双引号为止。
// 解决之道，可以使用惰性匹配：

var regex14 = /id=".*?"/;
var string14 = '<div id="container" class="main"></div>';
console.log(string14.match(regex14)[0]); //id="container"

// 当然，这样也会有个问题。效率比较低，因为其匹配原理会涉及到“回溯”这个概念（这里也只是顺便提一下，第四章会详细说明）。可以优化如下：
var regex15 = /id="[^"]*"/;
var string15 = '<div id="container" class="main"></div>';
console.log(string15.match(regex15)[0]); // id="container"

// 小结
// 字符匹配相关的案例，挺多的，不一而足。

// 掌握字符组和量词就能解决大部分常见的情形，也就是说，当你会了这二者，JS正则算是入门了。


